if
进行分支在上一章中,我们遇到了一个问题。我们如何使报告生成器脚本适应运行该脚本的用户的权限?解决这个问题需要我们根据测试结果找到一种在脚本中“改变方向”的方法。在编程方面,我们需要程序分支。
让我们考虑一个用伪代码表示的逻辑的简单例子,这是一种用于人类消费的计算机语言的模拟。
X = 5 If X = 5, then: Say “X equals 5.” Otherwise: Say “X doesis not equal 5.”
这是一个分支的例子。根据条件,“如果X=5”做一件事,“说X等于5”,否则做另一件事“说X不等于5”。
第二十七章:流量控制:使用 if
进行分支if
退出状态test
文件表达式字符串表达式整数表达式更现代的 test
版本(( )) - 专为整数设计组合表达式可移植性是小头脑的小妖精控制操作员:另一种分支方式总结
if
使用shell,我们可以对前面的逻辑进行如下编码:
xxxxxxxxxx
x=5
if [ "$x" -eq 5 ]; then
echo "x equals 5."
else
echo "x does not equal 5."
fi
或者我们可以直接在命令行中输入它(稍微缩短)。
xxxxxxxxxx
[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ "$x" -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ "$x" -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
does not equal 5
在这个例子中,我们执行命令两次;第一次,x的值设置为5,这会导致输出字符串“equals 5”,第二次,x值设置为0,这会输出字符串“does not equal 5”。
if
复合命令具有以下语法:
if commands; then commands [elif commands; then commands...] [else commands] fi
其中 commands 是命令列表。乍一看,这有点令人困惑。但在我们澄清这一点之前,我们必须看看shell如何评估命令的成功或失败。
命令(包括我们编写的脚本和shell函数)在终止时向系统发出一个值,称为退出状态(exit status)。此值是0到255范围内的整数,表示命令执行的成功或失败。按照惯例,零值表示成功,任何其他值表示失败。shell提供了一个参数,我们可以使用它来检查命令的退出状态。在这里,我们看到它在行动:
xxxxxxxxxx
[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2
在这个例子中,我们执行 ls
命令两次。第一次,命令成功执行。如果我们显示参数 $?
的值,我们看到它是零。我们再次执行 ls
命令(指定一个不存在的目录),产生错误,并再一次检查参数 $?
。这次它包含一个2,表示命令遇到了错误。一些命令使用不同的退出状态值来提供错误诊断,而许多命令在失败时只是以值1退出。手册页通常包含一个名为“Exit Status”的部分,描述使用的代码。然而,零总是意味着成功。
shell提供了两个极其简单的内置命令,除了以0或1退出状态终止外,什么也不做。 true
命令总是成功执行, false
命令总是失败执行。
xxxxxxxxxx
[me@linuxbox ~]$ true
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ false
[me@linuxbox ~]$ echo $?
1
我们可以使用这些命令来查看 if
语句是如何工作的。if语句真正做的是评估命令的成功或失败。
xxxxxxxxxx
[me@linuxbox ~]$ if true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if false; then echo "It's true."; fi
[me@linuxbox ~]$
命令 echo “It's true.”
在 if
命令成功执行时执行,在 if
命令未成功执行时不执行。如果命令列表遵循 if
,则计算列表中的最后一个命令:
xxxxxxxxxx
[me@linuxbox ~]$ if false; true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if true; false; then echo "It's true."; fi
[me@linuxbox ~]$
test
到目前为止, if
最常用的命令是 test
。 test
命令执行各种检查和比较。它有两种等效形式。第一个,如下所示:
test expression
第二种,更受欢迎的形式,如下所示:
[ expression ]
其中 expression 是被评估为真或假的表达式。当表达式为真时, test
命令返回退出状态0,当表达式为假时,返回状态1。
值得注意的是, test
和 [
实际上都是命令。在bash中,它们是内置的,但它们也作为程序存在于 /usr/bin 中,供其他shell使用。表达式实际上只是它的参数, [
命令要求 ]
字符作为其最终参数提供。
test
和 [
命令支持各种有用的表达式和测试。
下表列出了用于评估文件状态的表达式。
表达式 | 为true的情况 |
---|---|
file1 -ef file2 | file1 和 file2 具有相同的索引节点号 这两个文件名通过硬链接引用同一个文件。 |
file1 -nt file2 | file1 is newer than file2. |
file1 -ot file2 | file1 is older than file2. |
-b file | file存在,且是块专用(block-special,设备)文件。 |
-c file | file存在,且是一个字符特殊(character-special,设备)文件。 |
-d file | file存在,且是一个目录。 |
-e file | file存在。 |
-f file | file存在,且是一个普通文件。 |
-g file | file存在,且设置了组ID |
-G file | file存在,并由有效组ID拥有。 |
-k file | file存在,并且设置了“粘性位”。 |
-L file | file存在,是一个符号链接。 |
-O file | file存在,并由有效用户ID拥有。 |
-p file | file存在,并且是一个命名管道。 |
-r file | file存在并且可读(对有效用户具有可读权限)。 |
-s file | file存在并且长度大于零。 |
-S file | file存在,并且是一个网络套接字。 |
-t fd | fd 是指向/来自终端的文件描述符。 这可用于确定是否重定向了标准输入/输出/错误。 |
-u file | file存在并且是setuid。 |
-w file | file存在并且可写(对有效用户具有写权限)。 |
-x file | file存在并且可执行(对有效用户具有执行/搜索权限)。 |
这里我们有一个脚本,演示了一些文件表达式:
xxxxxxxxxx
# test-file: Evaluate the status of a file
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
exit
该脚本计算分配给常量 FILE
的文件,并在执行计算时显示其结果。关于这个脚本,有两点值得注意。
首先,请注意参数 $FILE
在表达式中的引用方式。这不是语法上完成表达式所必需的;相反,它是对参数为空或仅包含空格的防御。如果 $FILE
的参数扩展导致空值,则会导致错误(运算符将被解释为非空字符串而不是运算符)。在参数周围使用引号可确保运算符后面始终跟有字符串,即使字符串为空。
其次,请注意脚本末尾附近存在 exit
命令。 exit
命令接受一个可选参数,该参数将成为脚本的退出状态。当没有传递参数时,退出状态默认为最后执行的命令的退出状态。以这种方式使用 exit
允许脚本在 $FILE
扩展为不存在的文件名时指示失败。出现在脚本最后一行的 exit
命令是一种形式。当脚本“runs off the end”(到达文件末尾)时,它会以执行的最后一个命令的退出状态终止。
同样,shell函数可以通过在 return
命令中包含整数参数来返回退出状态。如果我们将之前的脚本转换为shell函数以将其包含在更大的程序中,我们可以用 return
语句替换 exit
命令并获得所需的行为。
xxxxxxxxxx
test_file () {
# test-file: Evaluate the status of a file
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
return 1
fi
}
下表列出了用于计算字符串的表达式:
表达式 | 为true的情况 |
---|---|
string | string 不为空 |
-n string | string 长度大于零 |
-z string | string 长度为零 |
string1 = string2 string1 == string2 | string1 和 string2 相等。 可以使用单等号或双等号。 bash支持使用双等号,通常是首选,但它不符合POSIX标准。 |
string1 != string2 | string1 和 string2 不同 |
string1 > string2 | string1 在 string2 之后排序。 |
string1 < string2 | string1 在 string2 之前排序。 |
警告:与测试一起使用时, >
和 <
表达式运算符必须加引号(或用反斜杠转义)。如果不是,它们将被shell解释为重定向运算符,可能会产生破坏性的结果。还要注意,虽然bash文档指出排序顺序符合当前区域设置的排序顺序,但可能不符合。ASCII(POSIX)顺序用于bash 4.0之前的版本。此问题已在4.1版本中修复。
这是一个包含字符串表达式的脚本:
xxxxxxxxxx
# test-string: evaluate the value of a string
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" == "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" == "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" == "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
在这个脚本中,我们计算常量ANSWER
。我们首先确定字符串是否为空。如果是,我们终止脚本并将退出状态设置为1。请注意应用于 echo
命令的重定向。这将错误消息“There is no answer.”重定向到标准错误,这是处理错误消息的正确方法。如果字符串不为空,我们会计算字符串的值,看看它是否等于“yes”、“no”或“maybe”。我们使用 elif
来实现这一点, elif
是 else if
的缩写。通过使用 elif
,我们能够构建更复杂的逻辑测试。
为了将值作为整数而不是字符串进行比较,我们可以使用下表中列出的表达式。
表达式 | 为true的情况 |
---|---|
integer1 -eq integer2 | integer1 等于 integer2 |
integer1 -ne integer2 | integer1 不等于 integer2 |
integer1 -le integer2 | integer1 小于或等于 integer2 (less than or equal) |
integer1 -lt integer2 | integer1 小于 integer2 (less than) |
integer1 -ge integer2 | integer1 大于或等于 integer2 (greater than or equal) |
integer1 -gt integer2 | integer1 大于 integer2 (greater than) |
以下是一个演示它们的脚本:
xxxxxxxxxx
# test-integer: evaluate the value of an integer.
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ "$INT" -eq 0 ]; then
echo "INT is zero."
else
if [ "$INT" -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
该脚本的有趣之处在于它如何确定整数是偶数还是奇数。通过对数字执行模2运算,将数字除以2并返回余数,它可以判断数字是奇数还是偶数。
test
版本现代版本的bash包含一个复合命令,可以作为 test
的增强替代品。它使用以下语法:
[[ expression ]]
其中,与 test
一样, expression 是一个计算结果为真或假的表达式。 [[ ]]
命令类似于 test
(它支持其所有表达式),但添加了一个重要的新字符串表达式。
string1 =~ regex
如果 string1 与扩展正则表达式 regex 匹配,则返回 true
。这为执行数据验证等任务开辟了很多可能性。在前面的整数表达式示例中,如果常量INT包含除整数之外的任何内容,则脚本将失败。脚本需要一种方法来验证常量是否包含整数。使用 [[ ]]
和 =~
字符表达式运算符,我们可以这样改进脚本:
xxxxxxxxxx
# test-integer2: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ "$INT" -eq 0 ]; then
echo "INT is zero."
else
if [ "$INT" -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
通过应用正则表达式,我们能够将 INT
的值限制为仅以可选减号开头,后跟一个或多个数字的字符串。此表达式还消除了空值的可能性。
[[ ]]
的另一个附加功能是 ==
运算符支持模式匹配,就像路径名扩展一样。这里有一个例子:
xxxxxxxxxx
[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'
这使得 [[ ]]
在评估文件名和路径名时非常有用。
除了 [[ ]]
复合命令外,bash还提供了 (( ))
复合命令,这对整数操作很有用。它支持一整套算术运算,我们将在 【第34章.字符串和数字】中全面介绍这一主题。
(( ))
用于执行算术真值测试(arithmetic truth tests)。如果算术求值的结果为非零,则算术真值测试结果为真。
xxxxxxxxxx
[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi
[me@linuxbox ~]$
使用 (( ))
,我们可以稍微简化 test-integer2 脚本,如下所示:
xxxxxxxxxx
# test-integer2a: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if ((INT == 0)); then
echo "INT is zero."
else
if ((INT < 0)); then
echo "INT is negative."
else
echo "INT is positive."
fi
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
请注意,我们使用小于和大于符号, ==
用于测试等价性。这是一种处理整数时看起来更自然的语法。还要注意,由于复合命令 (( ))
是shell语法的一部分,而不是普通命令,而且它只处理整数,因此它能够按名称识别变量,不需要执行扩展。我们将在【第34章】中进一步讨论 (( ))
和相关的算术展开。
还可以组合表达式来创建更复杂的计算。表达式通过使用逻辑运算符组合在一起。当我们学习 find
命令时,我们在【第17章.搜索文件】中看到了这些。 test
和 [[ ]]
有三个逻辑操作。它们是 AND
,OR
和 NOT
。 test
和 [[ ]]
使用不同的运算符来表示这些操作:
操作 | test | [[ ]] 和 (( )) |
---|---|---|
AND | -a | && |
OR | -o | || |
NOT | ! | ! |
这是一个 AND
操作的示例。以下脚本确定整数是否在值范围内:
xxxxxxxxxx
# test-integer3: determine if an integer is within a
# specified range of values.
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [[ "$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL" ]]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi
在这个脚本中,我们确定整数 INT
的值是否位于 MIN_VAL
和 MAX_VAL
的值之间。这是通过单次使用 [[ ]]
来执行的,其中包括由 &&
运算符分隔的两个表达式。我们也可以使用 test
对其进行编码:
xxxxxxxxxx
if [ "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" ]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
!
否定运算符反转表达式的结果。如果表达式为 false
,则返回 true
;如果表达式为 true
,则返回 false
。在下面的脚本中,我们修改了计算的逻辑,以找到指定范围之外的 INT
值:
xxxxxxxxxx
# test-integer4: determine if an integer is outside a
# specified range of values.
MIN_VAL=1
MAX_VAL=100
INT=50
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [[ ! ("$INT" -ge "$MIN_VAL" && "$INT" -le "$MAX_VAL") ]]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi
我们还在表达式周围加上括号,用于分组。如果不包括这些,否定只适用于第一个表达式,而不适用于两者的组合。使用测试对其进行编码的方式如下:
xxxxxxxxxx
if [ ! \( "$INT" -ge "$MIN_VAL" -a "$INT" -le "$MAX_VAL" \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
由于 test
使用的所有表达式和运算符都被shell视为命令参数(与 [[ ]]
和 (( ))
不同),因此对bash具有特殊含义的字符,如 <
、 >
、 (
和 )
,必须被引用或转义。
看到 test
和 [[ ]]
做大致相同的事情,哪一个更可取? test
是传统的(也是标准shell的POSIX规范的一部分,通常用于运行系统启动脚本),而 [[ ]]
特定于bash(和其他一些现代shell)。了解如何使用 test
很重要,因为它被广泛使用,但 [[ ]]
显然更有用,更容易编码,因此它是现代脚本的首选。
Hobgoblin —— 妖怪,大地精;淘气鬼,怪物;大哥布林
portability —— 可移植性,可携性
如果你和“真正的”Unix用户交谈,你很快就会发现他们中的许多人不太喜欢Linux。他们认为这是不洁的。Unix用户的一个原则是,一切都应该是“可移植的”。这意味着你写的任何脚本都应该能够在任何类Unix系统上运行,不受更改。
Unix用户有充分的理由相信这一点。在POSIX之前,他们已经看到了命令和shell的专有扩展对Unix世界的影响,他们自然对Linux对他们心爱的操作系统的影响持谨慎态度。
但可移植性有一个严重的缺点。它阻碍了进步。它要求事情总是使用“最低公分母”技术来完成。在shell编程的情况下,这意味着使所有内容都与 sh
兼容, sh
是原始的Bourne shell。
这种缺点是专有软件供应商用来为其专有扩展辩护的借口,只是他们称之为“创新”。但它们实际上只是客户的锁定设备。
GNU工具,如bash,没有这样的限制。它们通过支持标准和普遍可用来鼓励可移植性。你可以在几乎任何类型的系统上安装bash和其他GNU工具,甚至是Windows,而无需付费。所以,可以随意使用bash的所有功能。它真的很便携。
bash提供了两个可以执行分支的控制运算符。 &&
(AND)和 ||
(OR)运算符的工作方式与 [[ ]]
复合命令中的逻辑运算符相似。以下是 &&
的语法:
command1 && command2
以下是 ||
的语法:
command1 || command2
了解这些行为很重要。
使用&&运算符时,始终执行command1,只有当command1成功时才执行command2。
使用||运算符时,始终执行command1,只有当command1不成功时才执行command2。
实际上,这意味着我们可以做这样的事情:
xxxxxxxxxx
[me@linuxbox ~]$ mkdir temp && cd temp
这将创建一个名为 temp 的目录,如果成功,当前工作目录将更改为 temp 。只有当 mkdir
命令成功时,才会尝试执行第二个命令。同样,这样的命令:
xxxxxxxxxx
[me@linuxbox ~]$ [[ -d temp ]] || mkdir temp
将测试目录 temp 的存在,只有测试失败,才会创建目录。这种构造对于处理脚本中的错误非常方便,我们将在后面的章节中对此进行更多讨论。例如,我们可以在脚本中这样做:
xxxxxxxxxx
[ -d temp ] || exit 1
如果脚本需要临时目录,但该目录不存在,则脚本将终止,退出状态为1。
记住,如果我们有做复杂事情的冲动,命令可以是组命令:
xxxxxxxxxx
{ true && echo "true"; } && { false || echo "false"; }
组命令返回组中最后一个命令的退出状态。
我们以一个问题开始了这一章。我们如何使 sys_info_page 脚本检测用户是否有权读取所有主目录?根据我们对 if
的了解,我们可以通过将以下代码添加到 report_home_space
函数中来解决这个问题:
xxxxxxxxxx
report_home_space () {
if [[ "$(id -u)" -eq 0 ]]; then
cat << _EOF_
<h2>Home Space Utilization (All Users)</h2>
<pre>$(du -sh /home/*)</pre>
_EOF_
else
cat << _EOF_
<h2>Home Space Utilization ($USER)</h2>
<pre>$(du -sh $HOME)</pre>
_EOF_
fi
return
}
我们评估 id
命令的输出。使用 -u
选项, id
输出有效用户的数字用户 id
号。超级用户的ID始终为零,其他所有用户都是大于零的数字。知道这一点后,我们可以在这里构建两个不同的文档,一个利用超级用户权限,另一个仅限于用户自己的主目录。
我们将暂停 sys_info_page 程序,但别担心。它会回来的。与此同时,我们将讨论一些我们在恢复工作时需要的主题。